﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Web;
using System.Windows.Forms;
using CodePlex.WebServices.Client;
using HtmlDocument = HtmlAgilityPack.HtmlDocument;

namespace VSCPPublishPlugin.CodePlex
{
	public static class Api
	{
		public struct LoginToken_t
		{
			public static LoginToken_t Empty = new LoginToken_t(ProfileSettings_t.Empty, null)
											   {
												   _empty = true
											   };

			internal bool _empty;
			internal ProfileSettings_t _profile;
			internal CookieContainer _cookies;
			internal LoginToken_t(ProfileSettings_t profile, CookieContainer cookies)
			{
				_empty = false;
				_cookies = cookies;
				_profile = profile;
			}
		}
		public struct ProfileSettings_t
		{
			public static ProfileSettings_t Empty = new ProfileSettings_t()
			{
				ProfileName = "",
				Username = "",
				Password = "",
				ProjectName = "",
				CodePlexUrl = "",

				OpenReleasePageOnPublish = false,
			};

			public string ProfileName;
			public string Username;
			public string Password;
			public string ProjectName;
			public string CodePlexUrl;
			public bool OpenReleasePageOnPublish;
		}

		public class CodePlexRelease
		{
			public bool IsClickOnce
			{
				get;
				private set;
			}
			public int Id;
			public string Name;
			public string Description;
			public ReleaseStatus Status;
			public DateTime? Date;
			public bool IsDefault;
			public string ChangesetId;
			private byte[] _clickOnceFile;

			/*private IList<ReleaseFile> _files = new List<ReleaseFile>();

			public void AddFile(string filename, byte[] fileData, ReleaseFileType fileType)
			{
				var file = new ReleaseFile(filename, fileData, fileType);
			}*/

			public void SetClickOnceFile(Stream stream)
			{
				if (!stream.CanRead)
					throw new InvalidOperationException("Parameter 'stream' must be a readable Stream.");

				var len = stream.Length;
				_clickOnceFile = new byte[len];
				stream.Read(_clickOnceFile, 0, (int)len);
				IsClickOnce = true;
			}
			public void SetClickOnceFile(byte[] data)
			{
				if (null == data || 0 == data.Length)
					throw new InvalidOperationException("Parameter 'data' must contain some data.");

				_clickOnceFile = new byte[data.Length];
				data.CopyTo(_clickOnceFile, 0);
				IsClickOnce = true;
			}
			public void RemoveClickOnceFile()
			{
				_clickOnceFile = null;
				IsClickOnce = true;
			}
			public byte[] GetClickOnceFileData()
			{
				return _clickOnceFile;
			}
		}

		public static bool TryLogin(ProfileSettings_t profile, out LoginToken_t token)
		{
			const string loginUri = "https://www.codeplex.com/site/login?associate=None";

			var viewstate = HttpFormScraper.ExtractViewStateFromAspNetFormByUri(new Uri(loginUri), "__VIEWSTATE")[0];




			var cookies = new CookieContainer();
			cookies.Add(new Cookie("DetectIfCookiesAreEnabled", "aCookie", "/", ".codeplex.com"));
			cookies.Add(new Cookie("TimezoneOffset", "-7", "/", ".codeplex.com"));



			var formData = new Dictionary<string, HttpFormScraper.HtmlFormField_t>();
			formData.Add("__VIEWSTATE", new HttpFormScraper.HtmlFormField_t(viewstate));
			formData.Add("__EVENTTARGET", new HttpFormScraper.HtmlFormField_t(""));
			formData.Add("__EVENTARGUMENT", new HttpFormScraper.HtmlFormField_t(""));
			formData.Add("UserName", new HttpFormScraper.HtmlFormField_t(profile.Username));
			formData.Add("Password", new HttpFormScraper.HtmlFormField_t(profile.Password));
			formData.Add("remember", new HttpFormScraper.HtmlFormField_t("false"));
			formData.Add("loginButton", new HttpFormScraper.HtmlFormField_t("Sign In"));


			var webRequest = WebRequest.Create(loginUri) as HttpWebRequest;
			webRequest.CookieContainer = cookies;

			HttpWebResponse webResponse;

			string response;
			HttpFormScraper.PostHttpForm(webRequest, formData, out webResponse, out response);

			// if we didnt get redirected to home page, and the response doesnt want to redirect us to home page, un/pass is not correct
			if ("http://www.codeplex.com/" != webResponse.ResponseUri.AbsoluteUri)
			{
				if (!response.Contains("<title>Redirect</title>"))
				{
					token = LoginToken_t.Empty;
					return false;
				}
			}

			var tmpToken = new LoginToken_t(profile, webRequest.CookieContainer);
			var req = CreateWebRequest(GetPageUri(tmpToken), tmpToken);
			var resp = req.GetResponse();
			if ("http://www.codeplex.com/" == resp.ResponseUri.AbsoluteUri)
			{
				token = LoginToken_t.Empty;
				return false;
			}

			token = tmpToken;
			return true;
		}

		internal static IEnumerable<CodePlexRelease> GetReleases(LoginToken_t token)
		{
			if (token._empty)
			{
				throw new InvalidOperationException("Token is empty. Please get an authenticated token first!");
			}

			var req = CreateWebRequest(GetPageUri(token, "releases/"), token);
			var resp = req.GetResponse();
			var respUri = resp.ResponseUri.AbsoluteUri;
			var curRelId = ParseReleaseIdFromUri(respUri);


			var respRdr = new StreamReader(resp.GetResponseStream());
			var respHtml = respRdr.ReadToEnd();
			respRdr.Close();


			var doc = new HtmlDocument();
			doc.LoadHtml(respHtml);
			var releasesDivs = doc.DocumentNode.SelectNodes("//div[@id=\"ReleasesListDiv\"]/div");
			foreach (var releaseDiv in releasesDivs)
			{
				/*var relGroupStatusName = releaseDiv.GetAttributeValue("id", "");
				if (relGroupStatusName.Length > 4)
				{
					// e.g. "plannedList" -> remove the List
					relGroupStatusName = relGroupStatusName.Remove(relGroupStatusName.Length - 4);
					ReleaseStatus status;
					if( Enum.TryParse(relGroupStatusName,true,out status))
					{
						if(status)
					}
				}*/
				var releaseDivs = releaseDiv.SelectNodes("*//td[contains(@class, \"ReleaseCell\")]");
				if (null != releaseDivs)
				{
					foreach (var div in releaseDivs)
					{
						var release = new CodePlexRelease();
						bool ok = false;
						try
						{
							var node = div.SelectSingleNode("a");
							var releaseName = HttpUtility.HtmlDecode(node.InnerText.Trim()).Trim();
							var href = node.GetAttributeValue("href", "");

							release.Name = releaseName;
							if (0 == (release.Id = ParseReleaseIdFromUri(href)))
								release.Id = curRelId;

							node = div.SelectSingleNode("div");
							var relStatus = node.InnerText.Trim();
							if (!Enum.TryParse(relStatus, true, out release.Status))
							{
								release.Status = ReleaseStatus.None;
							}

							ok = true;
						}
						catch (Exception ex)
						{
							// FIXME: add logging
							MessageBox.Show(ex.ToString());
						}
						if (ok)
							yield return release;
					}
				}
			}
		}

		private static int ParseReleaseIdFromUri(string href)
		{
			int relId;
			var i = href.LastIndexOf("/");
			if (i >= 0)
			{
				href = href.Substring(i + 1);
				if (!int.TryParse(href, out relId))
					relId = 0;
			}
			else
			{
				relId = 0;
			}

			return relId;
		}

		public static bool TryGetReleases(LoginToken_t token, /*ReleaseStatus releaseStatus, */out IList<CodePlexRelease> releases)
		{
			IEnumerable<CodePlexRelease> rels;
			try
			{
				rels = GetReleases(token);
			}
			catch (InvalidOperationException ex)
			{
				releases = null;
				return false;
			}

			releases = new List<CodePlexRelease>(rels);
			return true;
		}

		public static bool CreateRelease(LoginToken_t token, CodePlexRelease release, bool replace = true)
		{
			if (!release.IsClickOnce)
				throw new InvalidOperationException("Can only publish ClickOnce releases for now!");

			// look up a release with the same name. If have - either do or dont replace
			var rels = GetReleases(token);
			var exists = rels.Any((r) => r.Name == release.Name);
			if (exists && !replace)
				return false;


			// FIXME: add support for non-ClickOnce releases)
			if (exists)
			{
				var existingRel = rels.First((r) => r.Name == release.Name);

				var mkReleaseUrl = GetPageUri(token, "releases/edit/" + existingRel.Id);

				var req = CreateWebRequest(mkReleaseUrl, token);
				var resp = req.GetResponse();
				var respStr = new StreamReader(resp.GetResponseStream()).ReadToEnd();
				var fields = HttpFormScraper.ExtractViewState(respStr, "__VIEWSTATE", "__EVENTVALIDATION");


				var request = CreateWebRequest(mkReleaseUrl, token);

				var formData = new Dictionary<string, HttpFormScraper.HtmlFormField_t>();
				formData.Add("__EVENTTARGET", new HttpFormScraper.HtmlFormField_t(""));
				formData.Add("__EVENTARGUMENT", new HttpFormScraper.HtmlFormField_t(""));
				formData.Add("__VIEWSTATE", new HttpFormScraper.HtmlFormField_t(fields[0]));
				formData.Add("__EVENTVALIDATION", new HttpFormScraper.HtmlFormField_t(fields[1]));
				formData.Add("ctl00$ctl00$MasterContent$ProjectSearchControl$SearchTextBox", new HttpFormScraper.HtmlFormField_t("Search all CodePlex projects"));
				formData.Add("Name", new HttpFormScraper.HtmlFormField_t(release.Name));
				formData.Add("AllowClickOnceSelection", new HttpFormScraper.HtmlFormField_t("False"));
				formData.Add("IsClickOnce", new HttpFormScraper.HtmlFormField_t("true"));
				formData.Add("ClickOnceLocation\"; filename=\"publish.zip", new HttpFormScraper.HtmlFormField_t(release.GetClickOnceFileData(), "Content-Type: application/x-zip-compressed"));
				formData.Add("MessageLineCount", new HttpFormScraper.HtmlFormField_t((4000 - release.Description.Length).ToString()));
				formData.Add("inputText", new HttpFormScraper.HtmlFormField_t(release.Description));
				formData.Add("MarkedForDeletion", new HttpFormScraper.HtmlFormField_t(""));
				formData.Add("MarkedAsRecommended", new HttpFormScraper.HtmlFormField_t(""));
				formData.Add("CurrentRecommendedFile", new HttpFormScraper.HtmlFormField_t(""));
				formData.Add("MultiFileUpload_0_FileName", new HttpFormScraper.HtmlFormField_t(""));
				formData.Add("MultiFileUpload_0_Type", new HttpFormScraper.HtmlFormField_t("RuntimeBinary"));
				formData.Add("MultiFileUpload_0_Location\"; filename=\"", new HttpFormScraper.HtmlFormField_t("", "Content-Type: application/octet-stream"));
				formData.Add("MultiFileUpload_0_Recommended", new HttpFormScraper.HtmlFormField_t("false"));
				formData.Add("DevelopmentStatus", new HttpFormScraper.HtmlFormField_t(GetReleaseStatusIndexFromEnum(release.Status).ToString()));
				formData.Add("ShowToPublic", new HttpFormScraper.HtmlFormField_t("true"));
				formData.Add("IsRecommended", new HttpFormScraper.HtmlFormField_t("false"));
				formData.Add("ReplaceCurrentRecommended", new HttpFormScraper.HtmlFormField_t("false"));
				formData.Add("ReleaseDate", new HttpFormScraper.HtmlFormField_t((release.Date ?? DateTime.Today).ToString("MM/dd/yyyy")));
				formData.Add("ChangesetId", new HttpFormScraper.HtmlFormField_t(release.ChangesetId));



				string responseData;
				HttpWebResponse response;
				HttpFormScraper.PostHttpForm(request, formData, out response, out responseData);
				var publishOk = response.ResponseUri.AbsoluteUri.Contains("/releases/view/" + existingRel.Id);
				if (!publishOk)
				{
					return false;
				}

				release.Id = existingRel.Id;
				return true;
			}

			return UploadClickOnceRelase(token._profile, release);
		}

		private static int GetReleaseStatusIndexFromEnum(ReleaseStatus status)
		{
			if (status == ReleaseStatus.Planning)
				return 1;

			if (status == ReleaseStatus.Alpha)
				return 2;

			if (status == ReleaseStatus.Beta)
				return 3;

			if (status == ReleaseStatus.Stable)
				return 4;

			return 0;
		}

		#region ClickOnce release upload

		private static bool UploadClickOnceRelase(ProfileSettings_t profile, CodePlexRelease release)
		{
			// since the CodePlex API doesnt yet support the CreateClickOnceRelease, will have to do this by POSTing the SOAP request

			var soapRequest = GenerateSoapRequest(profile.CodePlexUrl, release.Name, release.Description, release.Date ?? DateTime.Now, release.Status, release.IsDefault, release.ChangesetId, profile.Username, profile.Password, release.GetClickOnceFileData());

			string response = null;
			HttpWebResponse rsp = null;
			HttpWebRequest req = null;
			try
			{
				var reqUrl = new Uri("https://www.codeplex.com/services/ReleaseService.asmx");
				req = (HttpWebRequest)WebRequest.Create(reqUrl);
				req.Method = "POST";
				req.ContentType = "text/xml;charset=\"utf-8\"";
				req.Accept = "text/xml";

				var writer = new StreamWriter(req.GetRequestStream());
				writer.WriteLine(soapRequest);
				writer.Flush();

				rsp = (HttpWebResponse)req.GetResponse();
				var reader = new StreamReader(rsp.GetResponseStream());
				response = reader.ReadToEnd();
			}
			catch (WebException webex)
			{
				// 500 often means an invalid publish.zip file
				// 400 means that the guys at CodePlex messed up. I think. Only seen this one once, and not sure why :)
				// FIXME: add checking for errcode and give a possibly meaningfull error
				//ShowMessage("An exception occured while posting to CodePlex!", webex.ToString());
			}
			finally
			{
				if (null != req)
					req.GetRequestStream().Close();
				if (null != rsp)
					rsp.GetResponseStream().Close();
			}

			release.Id = -1;
			if (string.IsNullOrEmpty(response))
			{
				return false;
			}
			var soapBody = GetSurroundedString(response, "<soap:Body>", "</soap:Body>");
			if (string.IsNullOrEmpty(soapBody))
			{
				return false;
			}
			var createResponse = GetSurroundedString(soapBody, "<CreateClickOnceReleaseResponse xmlns=\"http://www.codeplex.com/services/ReleaseService/v1.0\">", "</CreateClickOnceReleaseResponse>");
			if (string.IsNullOrEmpty(createResponse))
			{
				// FIXME: parse out the error
				// no response element found -> probably an error occured
				//ShowMessage("An error occured. The response from the CodePlex API is below: ", soapBody);
				return false;
			}
			var releaseIdString = GetSurroundedString(createResponse, "<CreateClickOnceReleaseResult>", "</CreateClickOnceReleaseResult>");
			if (string.IsNullOrEmpty(releaseIdString))
			{
				//ShowMessage("Unable to get the release ID from the CodePlex API response. Below is the response:", createResponse);
				return false;
			}
			if (!int.TryParse(releaseIdString, out release.Id))
			{
				//ShowMessage("CodePlex API returned a release ID that is not an integer. Below is the Release ID returned: ", releaseIdString);
				release.Id = -1;
				return false;
			}

			return true;

			#region Using the API (when will become available)

			/*
			var releaseName = "ClickOnce";
			var releaseDescription = "The default clickonce release";

			// now publish to CodePlex
			ReleaseService srv = new ReleaseService();
			srv.Credentials = new NetworkCredential(profile.Username, profile.Password);
			releaseId = srv.CreateARelease(profile.ProjectName, releaseName, releaseDescription, DateTime.Now, ReleaseStatus.Planned, false, false);

			var releaseFiles = new List<ReleaseFile>();

			var releaseFile = new ReleaseFile("ClickOnce release file", "application/zip", "publish.zip", (byte[])null, ReleaseFileType.RuntimeBinary);
			releaseFile.FileData = File.ReadAllBytes(zipFilePath);
			releaseFiles.Add(releaseFile);

			srv.UploadReleaseFiles(profile.ProjectName, releaseName, releaseFiles);
			//*/

			#endregion
		}

		private static string GetSurroundedString(string input, string surroundStart, string surroundEnd)
		{
			string soapBody = null;
			var bodyStartIdx = input.IndexOf(surroundStart);
			var bodyEndIdx = input.IndexOf(surroundEnd);
			if (bodyStartIdx >= 0 && bodyEndIdx >= 0)
			{
				bodyStartIdx = bodyStartIdx + surroundStart.Length;
				if (input.Length > bodyStartIdx)
					soapBody = input.Substring(bodyStartIdx, bodyEndIdx - bodyStartIdx);
			}
			return soapBody;
		}


		private static string GenerateSoapRequest(string projectUrl, string releaseName, string releaseDescription, DateTime? releaseDate, ReleaseStatus releaseStatus, bool releaseIsDefault, string changesetId, string username, string password, byte[] zipData)
		{
			var fileData = Convert.ToBase64String(zipData);
			const string fmt = @"<?xml version='1.0' encoding='utf-8'?>
<soap12:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap12='http://www.w3.org/2003/05/soap-envelope'>
  <soap12:Body>
	<CreateClickOnceRelease xmlns='http://www.codeplex.com/services/ReleaseService/v1.0'>
	  <projectName>{0}</projectName>
	  <name>{1}</name>
	  <description>{2}</description>
	  <releaseDate>{3}</releaseDate>
	  <status>{4}</status>
	  <isDefaultRelease>{5}</isDefaultRelease>
	  <changesetId>{6}</changesetId>
	  <clickOnceFileBytes>{7}</clickOnceFileBytes>
	  <username>{8}</username>
	  <password>{9}</password>
	</CreateClickOnceRelease>
  </soap12:Body>
</soap12:Envelope>";

			string theDate = null;
			if (releaseDate.HasValue)
			{
				theDate = releaseDate.Value.ToString("dd MMMM, yyyy");
			}
			return string.Format(fmt, projectUrl, releaseName, releaseDescription, theDate, releaseStatus.ToString(), releaseIsDefault.ToString().ToLower(), changesetId, fileData, username, password);
		}

		#endregion


		internal static Uri GetPageUri(LoginToken_t token, string page = "")
		{
			return new Uri("http://" + token._profile.CodePlexUrl + ".codeplex.com/" + page ?? "");
		}
		internal static HttpWebRequest CreateWebRequest(string uri, LoginToken_t token)
		{
			return CreateWebRequest(new Uri(uri), token);
		}
		internal static HttpWebRequest CreateWebRequest(Uri uri, LoginToken_t token)
		{
			var webRequest = WebRequest.Create(uri) as HttpWebRequest;
			webRequest.Method = "GET";
			webRequest.CookieContainer = token._cookies;
			webRequest.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8";
			//webRequest.KeepAlive = true;

			return webRequest;
		}
	}
}
